package ai.knn;

import java.util.Iterator;
import java.util.Vector;

import ai.basic.AIController;
import ai.basic.Action;
import ai.basic.Fact;
import ai.basic.Pattern;

import util.essential.SourceGetter;
import util.math.PointCalculator;

public class AgentBrain implements AIController{
	//Nested Classes:
	private class NearestFactBean{
		private Fact fact = null;
		private double distance = -1;
		private boolean isInsertable = false;
		private boolean isReplaceable = false;
		
		/**
		 * @return the distance with respect the current query pattern.
		 */
		public double getDistance() {
			return distance;
		}
		public void setDistance(double distance) {
			this.distance = distance;
		}
		/**
		 * @return the nearest fact from the current query pattern.
		 */
		public Fact getFact() {
			return fact;
		}
		public void setFact(Fact fact) {
			this.fact = fact;
		}
		/**
		 * @return whether current query pattern is insertable depending on current "brain" status.
		 */
		public boolean isInsertable() {
			return isInsertable;
		}
		public void setInsertable(boolean isInsertable) {
			this.isInsertable = isInsertable;
		}
		/**
		 * @return whether this fact should be replaced with the current pattern. 
		 */
		public boolean isReplaceable() {
			return isReplaceable;
		}
		public void setReplaceable(boolean replaceIt) {
			this.isReplaceable = replaceIt;
		}
		
		
	}
	
	// Constants:
	public static final byte EXCECUTE = 0;

	public static final byte REPLACE_WHEN_NEW_PATTERN = 1;

	public static final byte NOT_REPLACE_WHEN_NEW_PATTERN = 2;

	public static final byte OPERATE_WITH_EUCLIDEAN_DISTANCE = 0;

	public static final byte OPERATE_WITH_SQUARE_ERROR = 1;
	
	public static final double DEFAULT_GRANULARITY = 20; /*It is a distance units between existent facts.*/
	
	public final String BRAIN_EXTENSION_FILE = ".kb"; //".kb" from "Knowledge base".

	// Attributes:
	private String brainName;
	
	private Vector<Fact> facts;

	private byte status; /*[EXCECUTE | REPLACE_WHEN_NEW_PATTERN | NOT_REPLACE_WHEN_NEW_PATTERN]*/

	private byte operationMode; /*[OPERATE_WITH_EUCLIDEAN_DISTANCE | OPERATE_WITH_SQUARE_ERROR]*/
	
	private double granularity;

	// Constructors:
	public AgentBrain(String brainName) {
		this.status = AgentBrain.EXCECUTE;
		this.init(brainName);
	}

	public AgentBrain(String brainName,byte status) {
		this.status = status;
		this.init(brainName);
	}

	private void init(String brainName) {
		this.brainName = brainName;
		this.operationMode = AgentBrain.OPERATE_WITH_EUCLIDEAN_DISTANCE;
		this.granularity = AgentBrain.DEFAULT_GRANULARITY;
		Vector<Fact> tmp = this.recoverBrain();
		if(tmp != null){
			this.facts = tmp;
		}else{
			this.facts = new Vector<Fact>();
		}
	}

	// Methods:
	/*
	 * (non-Javadoc)
	 * 
	 * @see ai.AIController#enterPatter(ai.Fact)
	 */
	public boolean enterPatter(Fact fact) {
		NearestFactBean nearestFactBean = this.getInsertability(fact);
		if (nearestFactBean != null && nearestFactBean.isInsertable()) {
			if(nearestFactBean.isReplaceable()){
				this.facts.remove(nearestFactBean.getFact());
			}
			this.facts.add(fact);
			System.out.println("Fact \""+fact.toString()+"\" has been entered.");
			System.out.println("\t - Number of facts = "+this.facts.size());
			this.saveBrain();
			return true;
		}
		System.err.println("Fact no entered!");
		return false;
	}

	/**
	 * This method allows to know whether it is possible to insert a given fact.
	 * 
	 * @param fact
	 * @return whether can insert a given fact.
	 */
	private NearestFactBean getInsertability(Fact fact) {
		NearestFactBean nearestFact = null;
		if (this.status != AgentBrain.EXCECUTE) {
			nearestFact = this.getNearestFact(fact.getPattern());
			if (nearestFact.getFact() != null) {
				if(nearestFact.getDistance() <= this.granularity){
					if(this.status == AgentBrain.REPLACE_WHEN_NEW_PATTERN){
						nearestFact.setInsertable(true);
						nearestFact.setReplaceable(true);
					}//else not replace it neither insert.
				}else{
					nearestFact.setInsertable(true);
					nearestFact.setReplaceable(false);
				}
			}else{
				nearestFact.setInsertable(true);
				nearestFact.setReplaceable(false);
			}
		}
		return nearestFact;
	}

	/**
	 * This method allows to get the nearest pattern from the current given
	 * pattern.
	 * 
	 * @param pattern
	 * @return the nearest fact from the given pattern.
	 */
	private NearestFactBean getNearestFact(Pattern pattern) {
		NearestFactBean ans = new NearestFactBean();
		ans.setFact(null); //Nearest fact from pattern.
		ans.setDistance(0); // minimum either distance or sqare error of nearest fact (depending on operation mode).
		double currentValue;
		boolean isInitialized = false;
		for (Iterator iter = this.facts.iterator(); iter.hasNext();) {
			Fact fact = (Fact) iter.next();
			if (!isInitialized) {
				ans.setDistance(this.getDistance(pattern
						.getValue(), fact.getPattern().getValue()));
				isInitialized = true;
			} else {
				currentValue = this.getDistance(pattern
						.getValue(), fact.getPattern().getValue());
				if ((currentValue < ans.getDistance()) && (Pattern.areEquals(pattern, fact.getPattern()))) {
					ans.setDistance(currentValue);
					ans.setFact(fact);
				}
			}
		}
		return ans;
	}
	
	/**
	 * This method allows to get the distance, depending on the
	 * mode selected: Either OPERATE_WITH_EUCLIDEAN_DISTANCE or OPERATE_WITH_SQUARE_ERROR. 
	 * @param a
	 * @param b
	 * @return distance, depending on the operation mode selected.
	 */
	private double getDistance(short[]a,short[]b){
		switch(this.operationMode){
			case AgentBrain.OPERATE_WITH_EUCLIDEAN_DISTANCE:{
				return PointCalculator.getEuclideanDistance(a,b);
			}
			case AgentBrain.OPERATE_WITH_SQUARE_ERROR:{
				/*
				 *  This method allows to get the fact with minimum square error with respect
				 * to the given pattern.
				 */
				return PointCalculator.getSquareError(a,b);
			}
			default:{
				return -1;
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see ai.AIController#query(ai.Pattern)
	 */
	public Action query(Pattern pattern) {
		NearestFactBean nearestFactBean = this.getNearestFact(pattern);
		if (nearestFactBean !=null && nearestFactBean.getFact()!=null) {
			System.out.println("Fact \""+nearestFactBean.getFact().toString()+"\" has been found. ("+nearestFactBean.getFact().getPattern().formToString()+")");
			return nearestFactBean.getFact().getAction();
		}else{
			System.err.println("No Fact found!");
			return null;
		}
	}

	/**
	 * @return operation mode. [OPERATE_WITH_EUCLIDEAN_DISTANCE | OPERATE_WITH_SQUARE_ERROR]
	 */
	public byte getOperationMode() {
		return operationMode;
	}

	/**
	 * @param operation mode. [OPERATE_WITH_EUCLIDEAN_DISTANCE | OPERATE_WITH_SQUARE_ERROR]
	 */
	public void setOperationMode(byte operationMode) {
		this.operationMode = operationMode;
	}

	/**
	 * @param status. [EXCECUTE | REPLACE_WHEN_NEW_PATTERN | NOT_REPLACE_WHEN_NEW_PATTERN]
	 */
	public void setStatus(byte status) {
		this.status = status;
	}

	/**
	 * @return status of the agent "brain".
	 */
	public byte getStatus() {
		return this.status;
	}

	/**
	 * This parameter is the distance between the existent facts.
	 * @return granularity.
	 */
	public double getGranularity() {
		return granularity;
	}

	/**
	 * This parameter is the distance between the existent facts.
	 * @param granularity
	 */
	public void setGranularity(double granularity) {
		this.granularity = granularity;
	}
	
	public void close(){
		this.saveBrain();
	}
	
	/**
	 * This method allows to save the knoledge about facts.
	 */
	private void saveBrain(){
		SourceGetter.config.replaceObjectInAFile(this.brainName+this.BRAIN_EXTENSION_FILE, this.facts);
	}
	/**
	 * @return set of recovered facts.
	 */
	private Vector<Fact> recoverBrain(){
		return (Vector<Fact>) SourceGetter.config.loadObjectFromAFile(this.brainName+this.BRAIN_EXTENSION_FILE);
	}
	
	private void listFact(){
		for (Iterator iter = this.facts.iterator(); iter.hasNext();) {
			Fact element = (Fact) iter.next();
			System.out.println("* "+element.toString());
		}
		System.out.println("TOTAL: "+this.facts.size()+" facts.");
	}
	
	//Main:
	public static void main(String[]args){
		AgentBrain agentBrain = null;
		String command,tmp;
		do{
			command = SourceGetter.in.getString("> ");
			if(command.equals("new")){
				System.out.print("\t Enter file name (without extention): ");
				tmp = SourceGetter.in.getString();
				agentBrain = new AgentBrain(tmp);
			}else if(command.equals("close")){
				agentBrain = null;
			}else if(command.equals("list")){
				if(agentBrain!=null){
					System.out.println(".::::::Facts::::::.");
					agentBrain.listFact();
					System.out.println(".::=============::.");
				}else{
					System.err.println("No brain! You have to create one firt.");
				}
			}else if(command.equals("query")){
				
			}else if(command.equals("help")){
				System.out.println(".::Commands::.");
				System.out.println("\t new: Creates a new \"brain\" from a given file.");
				System.out.println("\t close: Closes current \"brain\".");
				System.out.println("\t list: Lists the facts.");
				System.out.println("\t query: Query \"brain\" with a given pattern.");
				System.out.println("\t exit: Exits from this program.");
			}else if(command.equals("exit")){
				System.out.println("Bye!");
			}else{
				System.err.println("No command! Try again.");
			}
		}while(!command.equals("exit"));
	}
}
